package com.heima.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.model.user.pojos.ApUser;
import com.heima.search.service.ApUserSearchService;
import com.heima.search.service.ArticleSearchService;
//import com.heima.utils.thread.AppThreadLocalUtil;
import com.heima.utils.thread.AppThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private ApUserSearchService apUserSearchService;

    /**
     * es文章分页检索
     * @param dto
     * @return
     */
    @Override
    public ResponseResult search(UserSearchDto dto) throws IOException {

        //1.检查参数，搜索关键字不能为空
        if(dto == null || StringUtils.isBlank(dto.getSearchWords())){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        ApUser user = AppThreadLocalUtil.getUser();

//      异步调用(需要在insert上加上@Async) 保存搜索记录
        //user != null不登陆不能保存
        //TODO dto.getFromIndex() == 0说明是首页，第一次加载，如果当前搜索词是加载多页，无需保存多次????
//     不记录翻页的搜索：当用户在第一页搜索后，点击进行翻页（例如第二页的搜索结果），
//     此时 getFromIndex() 的返回值将是非零（如 10 或 20），表示用户在查看结果的后续部分。
//     这种情况下，一般不再记录搜索词，目的是为了避免将同一搜索词多次记录到数据库中，以减小数据重复和冗余。
        if(user != null && dto.getFromIndex() == 0){
            //因为insert方法是异步调用，说明是重新开了一个线程去执行该方法，
            // (只能从search主线程中获取当前用户)所以不能使用ThreadLocal去获取用户信息，所以需要从外面(search主线程)传userId进去
            apUserSearchService.insert(dto.getSearchWords(), user.getId());
        }//dto.getFromIndex() == 0 通常表示这是用户进行的第一次搜索请求（即在第一页的数据）。

        //2.设置查询条件，传索引库名称
        SearchRequest searchRequest = new SearchRequest("app_info_article");
        //所有条件都是通过searchSourceBuilder对象去创建的，
        // 最后把searchSourceBuilder直接放进searchRequest.source()里
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        //布尔查询(因为有多个查询条件)，queryStringQueryBuilder不能直接放进searchSourceBuilder里了，而是放进boolQueryBuilder里
        //然后把boolQueryBuilder放进searchSourceBuilder里
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        //关键字的分词之后查询，需要把dto.getSearchWords()拿到当前的title字段和content字段里查询
        QueryStringQueryBuilder queryStringQueryBuilder =
                QueryBuilders.queryStringQuery
                        (dto.getSearchWords())
                        .field("title")
                        .field("content")
                        //defaultOperator：分词后的一个条件是默认是AND，如果希望是OR，则设置成OR即可(这样就可以拿到更多的结果集)？？？
                        .defaultOperator(Operator.OR);
        //如果使用 AND，所有搜索词都必须出现在文档中，只有当文档同时包含所有搜索词时，该文档才会被返回。适合需要找到更精确匹配的场景。

//-----------------------------------------------------
        //因为有多个查询条件，queryStringQueryBuilder不能直接放进searchSourceBuilder里了，而是放进boolQueryBuilder里
        //然后把boolQueryBuilder放进searchSourceBuilder里
        //boolQueryBuilder里加入第一个查询条件，must 是布尔查询（Bool Query）的一部分，
        // 用于指定必须满足的查询条件。当你使用 must 时，你是在告诉 Elasticsearch，
        // 只有当文档满足所有在 must 中列出的查询条件时，该文档才会被认为是匹配查询
        // 强调了你的查询需要同时满足多个条件
        boolQueryBuilder.must(queryStringQueryBuilder);
        //.field("title").field("content"):这里您指定了要查询的字段，即 title 和 content。查询会在这两个字段中寻找分词后的文本。
        //只要字段中包含查询字符串中的任意词，就会被认为是匹配的。
        // 例如，如果搜索词是 "apple banana"，那么只要一个文档的 title 或 content 中包含 "apple" 或 "banana" 中的任意一个词，该文档就会匹配。
        //将构建的查询字符串查询添加到布尔查询的 must 部分。这表示您希望返回的文档必须符合这个查询条件。


//当你有一些需要的条件（使用 must）以及一些用于排除的条件（使用 filter）时，结合两者来构建查询

        //查询小于mindate的毫秒值(.getTime())的数据，mindate是在UserSearchDto里定义的minBehotTime
        RangeQueryBuilder rangeQueryBuilder =//rangeQuery按照范围去查，到publishTime发布时间这个域查
                QueryBuilders.rangeQuery("publishTime").lt(dto.getMinBehotTime().getTime());
        //往boolQueryBuilder里加入第二个查询条件
        //当你只需要确定文档是否符合某些条件，而不关心这些条件如何影响文档的相关性评分时，可以使用 filter。
        // 例如，范围查询通常放在 filter 中，因为我们只关心是否满足范围，而不是文档的相关性得分。
        //使用 filter 可以提高查询性能，因为 Elasticsearch 可以缓存 filter 的结果，
        // 从而加快后续相同查询的响应时间。而 must 查询则需要计算相关性得分，这通常需要更高的计算资源。
        boolQueryBuilder.filter(rangeQueryBuilder);//这样做的目的是将符合该范围条件的文档保留下来，而不关心这些文档的相关性评分。
//        使用 filter 的这个范围查询将会过滤掉所有 publishTime 大于或等于 dto.getMinBehotTime().getTime() 的文档。
//        这意味着，只有那些 publishTime 字段的值小于指定时间的文档才会被保留下来，其余的文档会被排除在查询结果之外。

        //分页查询，所有条件都是通过searchSourceBuilder对象去创建的
        searchSourceBuilder.from(0);//从第一条结果开始，因为是滚屏查询
        searchSourceBuilder.size(dto.getPageSize());//每页显示多少条

//当 from 值较大时，性能可能会下降，因为 Elasticsearch 需要跳过前面的结果。
// 在处理大量数据时，使用 from 进行深度分页可能不是最佳实践。
// 在这种情况下，可以考虑使用 search_after 或者基于某种排序条件的游标方式来进行高效分页。
// 默认情况下，from 的值是 0，如果未设置，它就会从第一条结果开始。

        //按照发布时间倒序查询
        searchSourceBuilder.sort("publishTime", SortOrder.DESC);

        //设置高亮  title
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("title");//标题域高亮
        //高亮前缀和后缀(是固定值)  跟前端端交互的时候要谈一谈
        highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");
        highlightBuilder.postTags("</font>");
        //把高亮设置进去
        searchSourceBuilder.highlighter(highlightBuilder);

        //因为有多个查询条件，queryStringQueryBuilder不能直接放进searchSourceBuilder里了，而是放进boolQueryBuilder里
        //然后把boolQueryBuilder放进searchSourceBuilder里
        searchSourceBuilder.query(boolQueryBuilder);
        //所有条件都是通过searchSourceBuilder对象去创建的，
        // 最后把searchSourceBuilder直接放进searchRequest.source()里
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//--------------------------------------------------

        //3.结果封装返回

        List<Map> list = new ArrayList<>();

        SearchHit[] hits = searchResponse.getHits().getHits();//包装了两层，最外层是hits，里面有total，hits，sort等
        //hits里有多个hit，hit里包含一个source，source里包含一个json串，就是文章信息
        for (SearchHit hit : hits) {
            //默认返回的json串
            String json = hit.getSourceAsString();
            //把这个json串转成map
            Map map = JSON.parseObject(json, Map.class);
            //处理高亮，因为高亮字段是动态的，可能没有，所以要判断一下
            if(hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0){
                Text[] titles = hit.getHighlightFields()
                        .get("title")//从当前的高亮字段里取title，
                        // 因为之前设置高亮的时候，是highlightBuilder.field("title");//标题域高亮，
                        // 因为可能多个高亮字段，所以用Text[]
                        .getFragments();//其它高亮碎片
                // 把高亮碎片拼接成一个字符串
                String title = StringUtils.join(titles);//高亮之后的标题(包含了前缀和后缀)
                //高亮标题
                // 把高亮之后的标题重新放到map里
                map.put("h_title",title);
            }else {
                //找不到高亮字段，用原始标题，不需要高亮
                map.put("h_title",map.get("title"));
            }
            list.add(map);
        }
        return ResponseResult.okResult(list);
    }
}
